package eval.engine.cplex.InitialCplexCode;

import ilog.concert.IloException;
import ilog.concert.IloIntMap;
import ilog.concert.IloIntVar;
import ilog.concert.IloNumMap;
import ilog.cplex.IloCplex;
import ilog.opl.IloOplDataSource;
import ilog.opl.IloOplErrorHandler;
import ilog.opl.IloOplFactory;
import ilog.opl.IloOplModel;
import ilog.opl.IloOplModelDefinition;
import ilog.opl.IloOplModelSource;
import ilog.opl.IloOplSettings;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.Properties;
import java.util.logging.Logger;

import org.omg.CORBA.Environment;

/**
 * @author Yosef Moatti
 * 
 * moatti@il.ibm.com
 *         
 */
public class CplexPlacementModel {
	int nbVms;
	int nbHosts;

	final static Class<CplexPlacementModel> sCLASS = CplexPlacementModel.class;
	final static String CLASSNAME = sCLASS.getName(); 
	final static Logger sLOGGER = Logger.getLogger("vsilogger.debug");
	private static int fileIndex = 0;

	final protected IloOplFactory oplFactory;
	protected IloCplex cplexSolver;
	protected IloOplErrorHandler errHandler;
	final protected CplexErrorLogger errLogger;
	protected IloOplModel model;

	int[][] vmOnHostMap;
	protected Environment environment;
	protected IloOplDataSource dataSource;

	// counts the number of times the method createNewModel is invoked
	private int modelCount = 0;

	/**
	 * @Constructor
	 * @param cplexInputString each line of this string defines one of the OPL input
	 */
	public CplexPlacementModel(String cplexInputString,int nbVms,int nbHosts) {
		IloOplFactory.setDebugMode(CplexAdapter.COLLECT_CPLEX_FILES_TO_FILE);  // used also as meaning that DEBUG CPLEX should be set 
		oplFactory = new IloOplFactory();

		this.errLogger = new CplexErrorLogger();
		this.errHandler = oplFactory.createOplErrorHandler(errLogger);

		// The index is used so that each call to the solver has a unique file.
		// Investigation should be done to determine if files need to be closed.
		fileIndex = fileIndex + 1;
		this.nbVms = nbVms;
		this.nbHosts = nbHosts;
		try {
			cplexSolver = oplFactory.createCplex();
			cplexSolver.setParam(IloCplex.IntParam.ParallelMode, -1); // Parallel alg. of CPLEX used

			//			Properties cplextParams = getCplexParams();
			//			setCplexParams(cplextParams);
		} catch (IloException e) {
			e.printStackTrace();
		}

		createNewModel();

		dataSource = new CplexDataSource(oplFactory, cplexInputString);

		if (!((CplexDataSource)dataSource).isUseCplexJNI()) {
			// the input to CPLEX was requested to be sent as a .dat file:
			((CplexDataSource)dataSource).customRead();
			String latestFileName = ((CplexDataSource)dataSource).generateNewDataFileName(); 
			IloOplDataSource fileDataSource = oplFactory.createOplDataSourceFromString(((CplexDataSource)dataSource).getDataString(), latestFileName);
			dataSource = fileDataSource;
		}  

		model.addDataSource(dataSource);
	}

	/**
	 * 
	 */
	public void createNewModel() {

		if (model != null) {
			model.resetSolutionGetter();
			model.delete();
		}

		IloOplModelSource modelSource = oplFactory
				.createOplModelSourceFromString(getModelStringRepresentation(), Integer.toString(modelCount));

		IloOplSettings settings = oplFactory.createOplSettings(errHandler);
		IloOplModelDefinition modelDefinition = oplFactory.createOplModelDefinition(modelSource, settings);
		model = oplFactory.createOplModel(modelDefinition, cplexSolver);

		modelCount++;
	}

	protected String getModelStringRepresentation() {

		StringBuffer stringBuffer = new StringBuffer(1000);

		try {
			InputStream inputStream = getClass().getResourceAsStream("oplUnivModel.mod");

			BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));

			int numRead = 0;
			char[] buf = new char[1024];
			while ((numRead = br.read(buf)) != -1) {
				String readData = String.valueOf(buf, 0, numRead);
				stringBuffer.append(readData);
				buf = new char[1024];
			}
			br.close();

		} catch (IOException e) {
			e.printStackTrace();
		}

		// setting the barrier algorithm
		try {
			cplexSolver.setParam(IloCplex.IntParam.RootAlg, IloCplex.Algorithm.Barrier);
		} catch (IloException e1) {
			e1.printStackTrace();
		}

		return stringBuffer.toString();
	}

	/**
	 * @return
	 */
	public CplexDataHolder getPlacement() {
		String METHOD = "getPlacement";

		model.generate();

		try {

			// Before solving the problem set up for debugging if debug on.
			// ------------------------------------------------------------
			if (CplexAdapter.COLLECT_CPLEX_FILES_TO_FILE) {
				String pathName = CplexAdapter.DIRECTORY_PATH; 

				if (pathName != null) {
					try {
						String fileIndexString = getFileIndexString();

						String exportModelFileName = "cplexModel_" + fileIndexString + ".sav";
						String paramFileName       = "cplexParam_" + fileIndexString + ".txt";
						String outName             = "cplexOut_"   + fileIndexString + ".txt";

						java.io.File             file = new java.io.File(pathName, outName);
						java.io.FileOutputStream fos  = new java.io.FileOutputStream(file);

						cplexSolver.exportModel(pathName + exportModelFileName);
						cplexSolver.writeParam(pathName  + paramFileName);
						cplexSolver.setOut(fos);
					} catch (Exception e) {
						sLOGGER.info("Exception: " + e.toString());            
					}
				} else { 
					sLOGGER.info(METHOD + " has a null pathName returned! ");            
				}
			}

			if (cplexSolver.solve()) {
				sLOGGER.info("---------------------------------------------");
				sLOGGER.info(" Solver status is " + cplexSolver.getStatus());
				sLOGGER.info("---------------------------------------------");

				IloIntMap solutionMatrix = model.getElement("targetPlacement").asIntMap();

				IloNumMap cpuAllocation = model.getElement("cpu_allocation").asNumMap();

				if (CplexAdapter.COLLECT_CPLEX_FILES_TO_FILE) {
					IloIntMap changeMatrix = model.getElement("changeMatrix").asIntMap();
					IloIntMap previouslyPlaced = model.getElement("previouslyPlaced").asIntMap();					
					IloNumMap relocationCost = model.getElement("relocationCost").asNumMap();
					IloIntMap emptyHost = model.getElement("emptyHost").asIntMap();

					//					printIntTab("placementPriority", placementPriority);
					//					printIntIntMatrix("Non 0 changeMatrix", changeMatrix, true);
					printIntTab("previouslyPlaced", previouslyPlaced);
					//					printNumTab("vmRelocated", vmRelocated);
					printNumTab("relocationCost", relocationCost);
					//					printIntTab("vmContainedHost", vmContainedHost);
					printIntTab("emptyHost", emptyHost);
				}

				//				sLOGGER.info(" =============> sumRelocationCost = " + sumRelocationCost);				

				if (CplexAdapter.COLLECT_CPLEX_FILES_TO_FILE) {
					IloNumMap cpuAllocOnHost = model.getElement("cpu_allocationOnHost").asNumMap();
					IloNumMap cpu_vsMarginOnHost = model.getElement("cpu_vsMarginOnHost").asNumMap();

					//					printDoubleMatrix("cpuAllocation", cpuAllocation);
					printNumTab("cpu_allocationOnHost", cpuAllocOnHost);
					printNumTab("cpu_vsMarginOnHost", cpu_vsMarginOnHost);
				}
				CplexDataHolder dataHolder = createPlacementObject( solutionMatrix, cpuAllocation);
				return dataHolder;
			} else {
				String msg = "Cplex failed to solve the problem.  Returned status is " + cplexSolver.getStatus();

				sLOGGER.info("=============================================");
				sLOGGER.info("=" + msg );
				sLOGGER.info("=============================================");

				throw new RuntimeException(msg);
			}

		} catch (IloException e) {
			// setScoringData_engineStop();

			e.printStackTrace();
		} finally { 
			// cleanup the CPLEX objects: 
			cplexSolver.end();
			errHandler.end();
			try { 
				errLogger.flush();
				errLogger.close();
			} catch (IOException e) {
				throw new RuntimeException("IOException caught while flushing/closing errLogger!", e);
			}
			model.end();
			model.delete();
			oplFactory.end();
		}
		return new CplexDataHolder(nbVms);
	}



	private void printIntTab(String prefixString, IloIntMap tab) {
		try {
			for (int j = 0; j < tab.getSize(); j++) {
				sLOGGER.info(" " + prefixString + "[" + j + "]=" + tab.get(j + 1) );
			}
		} catch (IloException e) {
			throw new RuntimeException(e);
		}
	}

	private void printNumTab(String prefixString, IloNumMap tab) {
		try {
			for (int j = 0; j < tab.getSize(); j++) {
				sLOGGER.info(" " + prefixString + "[" + j + "]=" + tab.get(j + 1) );
			}
		} catch (IloException e) {
			throw new RuntimeException(e);
		}
	}

	private CplexDataHolder createPlacementObject( IloIntMap solutionMatrix, IloNumMap cpuAllocation)
			throws IloException {

		int[][] curPlacement = new int[nbVms][nbHosts] ;
		int[] curActiveHosts = new int[nbHosts];

		// creating the placement elements
		for (int vmIndex = 0; vmIndex < nbVms; vmIndex++) {
			IloIntMap tempVSPlacement = solutionMatrix.getSub(vmIndex + 1);

			for (int hostIndex = 0; hostIndex < nbHosts; hostIndex++) {
				if (tempVSPlacement.get(hostIndex + 1) == 1) {
					curPlacement[vmIndex][hostIndex] = 1;
					//TODO: need to take the real value from Cplex, once it will be done.
					curActiveHosts[hostIndex] = 1;
				}
			}
		}

		// Force one of the non active hosts (if exists) to be active:
		boolean goOn = true;
		for (int hostIndex = 0; goOn && hostIndex < nbHosts; hostIndex++) {
			if (curActiveHosts[hostIndex] == 0) {
				curActiveHosts[hostIndex] = 1;
				goOn = false;
			}
		}

		return new CplexDataHolder(curPlacement,curActiveHosts) ;
	}


	/**
	 * Sets the CPLEX params to the values retrieved within passed parameters map
	 * @param cplexParams the parameters map
	 */
	private void setCplexParams(Properties cplexParams) {
		// TODO use reflection here:
		for (Object nextKey : cplexParams.keySet()) {
			String nextParam = (String)nextKey;
			try { 
				if ("EpGap".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.DoubleParam.EpGap, getDouble((String)cplexParams.get(nextParam))); // Relative relaxation
				} else if ("WorkMem".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.DoubleParam.WorkMem, getDouble( (String)cplexParams.get(nextParam))); // Absolute relaxation
				} else if ("EpAGap".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.DoubleParam.EpAGap, getDouble( (String)cplexParams.get(nextParam))); // Absolute relaxation
				} else if ("TreLim".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.DoubleParam.TreLim, getDouble( (String)cplexParams.get(nextParam))); 
				} else if ("TuningTiLim".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.DoubleParam.TuningTiLim,  getDouble( (String)cplexParams.get(nextParam)));
				} else if ("TiLim".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.DoubleParam.TiLim,  getDouble( (String)cplexParams.get(nextParam)));
				} else if ("IntSolLim".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.IntParam.IntSolLim,  getInteger( (String)cplexParams.get(nextParam))); 
				} else if ("NodeLim".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.IntParam.NodeLim, getInteger( (String)cplexParams.get(nextParam)));
				} else if ("ItLim".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.IntParam.ItLim, getInteger( (String)cplexParams.get(nextParam)));
				} else if ("NodeFileInd".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.IntParam.NodeFileInd, getInteger( (String)cplexParams.get(nextParam)));
				} else if ("NodeSel".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.IntParam.NodeSel, getInteger( (String)cplexParams.get(nextParam)));
				} else if ("WorkDir".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.StringParam.WorkDir,  (String)cplexParams.get(nextParam));
				} else if ("Threads".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.IntParam.Threads,  getInteger( (String)cplexParams.get(nextParam)));
				} else if ("dataCheck".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.BooleanParam.DataCheck,  getBoolean( (String)cplexParams.get(nextParam)));
				} else if ("Probe".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.IntParam.Probe,  getInteger( (String)cplexParams.get(nextParam)));
				} else if ("CoeRedInd".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.IntParam.CoeRedInd,  getInteger( (String)cplexParams.get(nextParam)));
				} else if ("PreInd".equals(nextParam)) {
					cplexSolver.setParam(IloCplex.BooleanParam.PreInd, getBoolean( (String)cplexParams.get(nextParam)));			
				} else {
					sLOGGER.info(" Float param not recognized! " + nextParam + " with requested value " + cplexParams.get(nextParam));
				}



			} catch (Exception e) {
				sLOGGER.info(" Unexpected exception thrown " + e);
				// skip
			}
		}
	}

	/**
	 * @return
	 */
	public static String getFileIndexString() {
		String fileIndexString = Integer.valueOf(fileIndex).toString();
		if (fileIndex < 10) {
			fileIndexString = "000" +  fileIndexString;
		} else if (fileIndex < 100) {
			fileIndexString = "00" +  fileIndexString;
		} else if (fileIndex < 1000) {
			fileIndexString = "0" +  fileIndexString;
		} 

		return fileIndexString;
	}

	private Integer getInteger(String value) {
		try {
			return Integer.parseInt(value);
		} catch (NumberFormatException nfe) {
			// skip this value
			sLOGGER.info(" Supposed integer value " + value + " ignored");
			throw new NumberFormatException(value);
		}
	}

	private Boolean getBoolean(String value) {
		// The boolean returned represents the value true if:
		// 1. the string argument is not null 
		// 2. is equal, ignoring case, to the string "true". 
		return Boolean.parseBoolean(value);
	}

	private double getDouble(String value) {
		try {
			return Double.parseDouble(value);
		} catch (NumberFormatException nfe) {
			try { 
				Float resFloat = Float.parseFloat(value);
				return 1.0d * resFloat;
			} catch (NumberFormatException nfe2) {
				try {
					int res = Integer.parseInt(value);
					return 1.0d * res;
				} catch (Exception e) {
					// skip this value
					sLOGGER.info(" Value " + value + " ignored");
					throw new NumberFormatException(value);
				}
			}
		}
	}


	/**
	 * Internal class for directing CPLEX errors to the configured logger 
	 * @author erezh
	 */
	protected static class CplexErrorLogger extends OutputStream {

		private StringBuffer sb = new StringBuffer();

		@Override
		public void close() throws IOException {
			sLOGGER.info(LOGGER_PREFIX + "close()");
			super.close();
		}

		static private final String LOGGER_PREFIX = "CplexErrorLogger ==> ";
		static private final String CPLEX_ERROR_DELIMITER = "\n";

		@Override
		public void write(byte[] b) throws IOException {

			String s = new String(b);
			String[] tokens = s.split(CPLEX_ERROR_DELIMITER);

			tokens[0] = sb.toString() + tokens[0];
			sb = new StringBuffer();

			for (int index = 0; index < tokens.length -1; index++) {
				sLOGGER.info(LOGGER_PREFIX + tokens[index]);
			}

			// Special handling for the last one:
			String last = tokens[tokens.length -1];
			if (s.endsWith(CPLEX_ERROR_DELIMITER)) {
				sLOGGER.info(LOGGER_PREFIX + last);
			} else {
				sb.append(last);
			}
		}

		@Override
		public void write(byte[] b, int off, int len) throws IOException {

			String s = new String(b, off, len);
			sLOGGER.info(LOGGER_PREFIX + "write(buf, off, len): " + s);
		}

		@Override
		public void flush() throws IOException {

			sLOGGER.info(LOGGER_PREFIX + "flush() ");
		}

		@Override
		public void write(int b) throws IOException {
			byte[] ba = new byte[1];
			ba[0] = (byte)b;

			String s = new String(ba);
			sLOGGER.info(LOGGER_PREFIX + "write(byte): " + s);
		}
	}



}


